﻿using System.CodeDom;
using JBeamLibrary.Exceptions;
using JBeamLibrary.VehicleParts.Elements;
using JBeamLibrary.VehicleParts.Enums;
using JBeamLibrary.VehicleParts.Groups;
using JBeamLibrary.VehicleParts.Interfaces;
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace JBeamLibrary.Tools
{
    public static class GroupHelper
    {
        /// <summary>Returns the closest index that has a value present in the list</summary>
        /// <param name="list"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public static int GetParentIndex(IDictionary list, int index)
        {
            while (true)
            {
                if (list.Contains(index))
                    return index;
                if (index < 0)
                    return -1;
                index = index - 1;
            }
        }

        /// <summary>Returns a list of property lists for a given group</summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public static IEnumerable<IDictionary> GetPropertyLists<T>(IVehicleGroup<T> group) where T : IVehicleElement
        {
            var list = new List<IDictionary>();
            if (group == null) return list.AsEnumerable();

            var props = group.GetType().GetProperties();
            list.AddRange(from prop in props where typeof(IDictionary).IsAssignableFrom(prop.PropertyType) select prop.GetValue(@group) as IDictionary);

            return list.AsEnumerable();
        }

        /// <summary>Returns a list of grouping property lists for a given group</summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public static IEnumerable<IDictionary> GetGroupingPropertyLists<T>(IVehicleGroup<T> group) where T : IVehicleElement
        {
            var list = new List<IDictionary>();
            if (group == null) return list.AsEnumerable();

            var props = group.GetType().GetProperties();
            list.AddRange(from prop in props where typeof(IDictionary).IsAssignableFrom(prop.PropertyType) && prop.Name.ToUpperInvariant().EndsWith("GROUPLIST") select prop.GetValue(@group) as IDictionary);

            return list.AsEnumerable();
        }

        /// <summary>Returns a list of property list names</summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public static IEnumerable<string> GetPropertyListNames<T>(IVehicleGroup<T> group) where T : IVehicleElement
        {
            var list = new List<string>();
            if (group == null) return list.AsEnumerable();

            var props = group.GetType().GetProperties();
            list.AddRange(from prop in props where typeof(IDictionary).IsAssignableFrom(prop.PropertyType) select prop.Name);

            return list.AsEnumerable();
        }

        /// <summary>Returns a list of grouping property list names</summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public static IEnumerable<string> GetGroupingPropertyListNames<T>(IVehicleGroup<T> group) where T : IVehicleElement
        {
            var list = new List<string>();
            if (group == null) return list.AsEnumerable();

            var props = group.GetType().GetProperties();
            list.AddRange(from prop in props where typeof(IDictionary).IsAssignableFrom(prop.PropertyType) && prop.Name.ToUpperInvariant().EndsWith("GROUPLIST") select prop.Name);

            return list.AsEnumerable();
        }

        /// <summary>Returns a property list of the provided group object</summary>
        /// <param name="group"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        public static IDictionary GetPropertyList<T>(IVehicleGroup<T> group, string name) where T : IVehicleElement
        {
            var prop = group.GetType().GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
            if (prop == null) return null;

            return prop.GetValue(group) as IDictionary;
        }

        /// <summary>Gets the largest index of all lists in a group including the group itself</summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public static int GetGroupHighIndex<T>(IVehicleGroup<T> group) where T : IVehicleElement
        {
            return GetPropertyLists(@group).Select(list => (list.Count > 0 ? list.Keys.OfType<int>().Max() : 0) + 1).Concat(new[] { @group.Count }).Max();
        }

        /// <summary>Returns the property name matching a given property list name</summary>
        /// <param name="propertyListName"></param>
        /// <returns></returns>
        public static string GetPropertyName(string propertyListName)
        {
            return propertyListName.Substring(0, propertyListName.LastIndexOf("List")).Propertize();
        }

        /// <summary>Returns the value of a property in the given list</summary>
        /// <param name="list"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public static object GetPropertyValue(IDictionary list, int index)
        {
            var parentIndex = GetParentIndex(list, index);
            return parentIndex >= 0 ? list[parentIndex] : GetPropertyDefault(list);
        }

        /// <summary>Returns the value of a property in the given list, or null if none could be found</summary>
        /// <param name="list"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        public static object GetPropertyValueOrNull(IDictionary list, int index)
        {
            var parentIndex = GetParentIndex(list, index);
            return parentIndex >= 0 ? list[parentIndex] : null;
        }

        /// <summary>Returns the default value for a list</summary>
        /// <param name="list"></param>
        /// <returns></returns>
        public static object GetPropertyDefault(IDictionary list)
        {
            var args = list.GetType().GetGenericArguments();
            var valType = args[1];
            return valType == typeof(string) ? string.Empty : Activator.CreateInstance(valType);
        }

        /// <summary>Sets the value of a property of a property list</summary>
        /// <param name="list"></param>
        /// <param name="index"></param>
        /// <param name="value"></param>
        public static void SetPropertyValue(IDictionary list, int index, object value)
        {
            var args = list.GetType().GetGenericArguments();
            var valType = args[1];
            var val =
                value is JValue
                ? ((JValue)value).ToListVal(list)
                : Convert.ChangeType(value, valType);

            if (!list.Contains(index))
                list.Add(index, val);
            else
                list[index] = val;
        }

        /// <summary>Closes the pgrouping properties left open at the end of a section</summary>
        /// <param name="group"></param>
        /// <param name="groupArray"></param>
        public static void CloseGroupingProperties<T>(IVehicleGroup<T> group, JArray groupArray) where T : IVehicleElement
        {
            /* Functionality disabled */
            if (!Config.AutoCloseGroupingProperties)
                return;

            /* Close grouping properties if needed */
            var gProps = new JObject();
            foreach (var groupingListName in GetGroupingPropertyListNames(group))
            {
                /* If the list has defined elements (group, deformGroup, ...) then close the group if it isn't already */
                var list = GetPropertyList(group, groupingListName);
                if (list.Count > 0 && !list.Contains(group.Count))
                {
                    gProps.Add(new JProperty(GetPropertyName(groupingListName), list[group.Count].ToJBeam()));
                }
            }

            /* Close grouping properties */
            if (gProps.Count > 0)
                groupArray.Add(gProps);
        }


        /* IN TEST */

        public static ElementProperties GetPropertyFromName(string propertyName)
        {
            ElementProperties prop;
            if (!Enum.TryParse(propertyName.Capitalize(), true, out prop))
                throw new UnknownPropertyException("unknown", "unknown", propertyName);
            return prop;
        }

        public static string GetNameFromProperty(ElementProperties property)
        {
            return property.ToStringOrEmpty().Propertize();
        }

        public static void CloseGroupingProperties<T>(VehicleSection<T> section) where T : class, IVehicleElement
        {
            /* Functionality disabled */
            if (!Config.AutoCloseGroupingProperties)
                return;

            /* No properties */
            if (!section.HasProperties)
                return;

            /* Add grouping properties at the end of the prop lists */
            //var props = section.Properties[section.LastPropertyIndex];
            foreach (var @group in ElementPropertyMaps.GroupingProperties)
            //.Where(@group => !props.Contains(@group))
            //.Where(@group => section.HasDefinedProperty(@group, ElementPropertyUse.Group))
            //.Where(@group => section.GetLastPropertyValue(@group, ElementPropertyUse.Group) != null))
            {
                /* Unused property */
                if (!section.HasDefinedProperty(@group, ElementPropertyUse.Group)) continue;

                /* Get last value */
                var lastValue = section.GetLastPropertyValue(@group, ElementPropertyUse.Group).ToStringOrEmpty();
                if (string.IsNullOrEmpty(lastValue)) continue;

                /* Add one at the end */
                section.Properties.SetPropertyValue(section.Count, @group, ElementPropertyUse.Group, string.Empty);
                //props.Add(new ElementProperty(@group, ElementPropertyUse.Group, string.Empty));
            }
        }

        public static int GetParentIndex<T>(VehicleSection<T> section, int index, ElementProperties prop, ElementPropertyUse use) where T : class, IVehicleElement
        {
            var props = section.Properties;

            /* There is an inline property defined for this one */
            if (props.Contains(index) && use == ElementPropertyUse.Group &&
                props[index].InlineProperties.Any(p => p.Property == prop)) return -1;

            /* Go up */
            while (true)
            {
                if (props.Contains(index) && props[index].GroupProperties.Any(p => p.Property == prop))
                    return index;
                if (index < 0)
                    return -1;
                index = index - 1;
            }
        }

        public static object GetPropertyValue<T>(VehicleSection<T> section, int index, ElementProperties prop, ElementPropertyUse use) where T : class, IVehicleElement
        {
            var props = section.Properties;
            var parentIndex = use == ElementPropertyUse.Group ? GetParentIndex(section, index, prop, use) : index;
            var property = props.Contains(parentIndex)
                ? props[parentIndex].Where(p => p.Use == use).FirstOrDefault(p => p.Property == prop)
                : null;
            return property != null ? property.Value : GetPropertyNullValue(prop);
        }

        public static object GetPropertyValue<T>(VehicleSection<T> section, int index, string propertyName, ElementPropertyUse use) where T : class, IVehicleElement
        {
            var prop = GetPropertyFromName(propertyName);
            return GetPropertyValue(section, index, prop, use);
        }

        public static object GetPropertyDefault(ElementProperties prop)
        {
            var valType = ElementPropertyTypes.TypeMap[prop];
            return valType == typeof(string) ? string.Empty : Activator.CreateInstance(valType);
        }

        public static object GetPropertyNullValue(ElementProperties prop)
        {
            return ElementPropertyTypes.NullValues[ElementPropertyTypes.TypeMap[prop]];
        }

        public static IEnumerable<ElementProperty> GetElementProperties<T>(VehicleSection<T> section, T element) where T : class, IVehicleElement
        {
            var props = section.Properties;
            var list = new List<ElementProperty>();
            var index = section.IndexOfElement(element);
            foreach (var group in props)
            {
                /* Register inline properties (exact same index as given element) */
                if (group.ElementIndex == index)
                    list.AddRange(group.InlineProperties);

                foreach (var prop in group.GroupProperties)
                {
                    /* Skip properties already registered */
                    if (list.Any(p => p.Property == prop.Property)) continue;

                    /* Get closest property of the given type */
                    var parentIndex = GetParentIndex(section, index, prop.Property, prop.Use);
                    if (!props.Contains(parentIndex)) continue;

                    /* Register property */
                    list.Add(prop);
                }
            }
            return list;
        }
    }
}
